﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BobcatMultiTouch;
using BobcatWiiLib;
using BobcatUtility;
using System.Xml;
using System.Drawing;
using System.Reflection;
using System.IO;
using System.Windows.Forms;

namespace MultiTouchSource_WiimoteIR
{
    /// <summary>
    /// This class wraps the CalibratedIRManager which receives wiimote IR events, and
    /// supports the IMultiTouchSource interface so that the IR events can be used via
    /// the MultiTouchInputManager (MTIM).
    /// </summary><remarks>
    /// Note: The events raised by the CalibratedIRManager occur on a background thread,
    ///       so when we raise these events to the MTIM they will be on the same thread.
    /// </remarks>
    public class WiimoteIRSource : IMultiTouchSource
    {
        #region Events

        // Events needed by the IMultiTouchSource interface...
        public event EventHandler<TouchArgs> TouchDownEvent;
        public event EventHandler<TouchArgs> TouchUpEvent;
        public event EventHandler<TouchArgs> TouchMoveEvent;
        public event EventHandler<ConfigChangedArgs> ConfigChangedEvent;

        #endregion

        #region Public methods

        /// <summary>
        /// Constructor.
        /// </summary>
        public WiimoteIRSource()
        {
        }

        /// <summary>
        /// We shut down the connection to the wiimote.
        /// </summary>
        public void close()
        {
            if (m_IRManager == null)
            {
                // The source was never initialized...
                return;
            }
            m_IRManager.IRStateChangedEvent -= onIRStateChanged;
            m_IRManager.IRPositionChangedEvent -= onIRPositionChanged;
            m_IRManager.ConfigChangedEvent -= onIRConfigChanged;
            m_IRManager = null;
        }

        /// <summary>
        /// Initializes the wiimote (including calibration data) from the XML passed in.
        /// </summary>
        public void initializeFromXMLConfig(string strXML)
        {
            m_IRManager = new CalibratedIRManager();

            // We read the config...
            string strCalibrationData;
            string strCalibrationImageFilename;
            int iSmoothingLevel;
            bool bRaiseMouseEvents;
            parseXMLConfig(strXML, out strCalibrationData, out strCalibrationImageFilename, out iSmoothingLevel, out bRaiseMouseEvents);

            // We set the smoothing level...
            if (iSmoothingLevel != 0)
            {
                m_IRManager.SmoothingLevel = iSmoothingLevel;
            }

            // We set the calibration image filename...
            m_IRManager.CalibrationImageFilename = strCalibrationImageFilename;

            // We set whether we are raising mouse events...
            m_IRManager.RaiseMouseEvents = bRaiseMouseEvents;

            // We set the calibration if we've got data, or show the calibration screen if
            // we haven't...
            if (strCalibrationData != "")
            {
                // We've got calibration data...
                m_IRManager.setCalibrationData(strCalibrationData);
            }
            else
            {
                // We haven't got calibration data, so we show the calibration screen...
                m_IRManager.showCalibrationScreen();
            }

            // We resgister for IR events...
            m_IRManager.IRStateChangedEvent += onIRStateChanged;
            m_IRManager.IRPositionChangedEvent += onIRPositionChanged;
            m_IRManager.ConfigChangedEvent += onIRConfigChanged;
        }

        /// <summary>
        /// Returns the config information used by this source as XML.
        /// </summary>
        public string getXMLConfig()
        {
            // The config needs to hold the params needed by the CalibratedIRManager.
            string strInnerXML = "";
            if (m_IRManager != null)
            {
                strInnerXML += XMLUtility.makeElement(CALIBRATION_DATA_ELEMENT, m_IRManager.getCalibrationData());
                strInnerXML += XMLUtility.makeElement(SMOOTHING_LEVEL_ELEMENT, m_IRManager.SmoothingLevel);
                strInnerXML += XMLUtility.makeElement(CALIBRATION_IMAGE_FILENAME_ELEMENT, m_IRManager.CalibrationImageFilename);
                strInnerXML += XMLUtility.makeElement(RAISE_MOUSE_EVENTS_ELEMENT, m_IRManager.RaiseMouseEvents);
            }

            string strXMLConfig = XMLUtility.makeElement(ROOT_ELEMENT, strInnerXML);
            return strXMLConfig;
        }

        /// <summary>
        /// Returns the name of this source.
        /// </summary>
        public string getSourceName()
        {
            return "Wiimote IR";
        }

        /// <summary>
        /// Returns the number of points supported by this source (i.e. 4)
        /// </summary>
        public int getMaxNumberOfPoints()
        {
            return 4;
        }

        /// <summary>
        /// Returns the icon for this source.
        /// </summary>
        public Image getIcon()
        {
            return Utility.getImageFromResources("wiimote_ir.png");
        }

        /// <summary>
        /// Returns the ConfigControl that lets you edit this source's settings.
        /// </summary>
        public UserControl getConfigControl()
        {
            return new ConfigControl(this);
        }

        /// <summary>
        /// Returns the CalibratedIRManager we're wrapping.
        /// </summary>
        public CalibratedIRManager getIRManager()
        {
            return m_IRManager;
        }

        #endregion

        #region Private functions


        /// <summary>
        /// We parse the XML data passed in, which should hold config in the format we expect. It is 
        /// possible that not all the fields are in the XML. If this is the case, we return 'empty'
        /// values for these fields.
        /// </summary>
        void parseXMLConfig(string strXML, out string strCalibrationData, out string strCalibrationImageFilename, out int iSmoothingLevel, out bool bRaiseMouseEvents)
        {
            // We set the data to 'empty' values...
            strCalibrationData = "";
            strCalibrationImageFilename = "";
            iSmoothingLevel = 0;
            bRaiseMouseEvents = false;

            // If there is no config, we just return the empty values...
            if (strXML == "")
            {
                return;
            }

            // We parse the XML...
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.InnerXml = strXML;
            XmlNode rootNode = xmlDocument.SelectSingleNode(ROOT_ELEMENT);
            if (rootNode == null)
            {
                // The XML does not contain our config...
                return;
            }

            // We look for the fields we're expecting...
            strCalibrationData = XMLUtility.getElementStringValue(CALIBRATION_DATA_ELEMENT, rootNode);
            strCalibrationImageFilename = XMLUtility.getElementStringValue(CALIBRATION_IMAGE_FILENAME_ELEMENT, rootNode);
            iSmoothingLevel = XMLUtility.getElementIntValue(SMOOTHING_LEVEL_ELEMENT, rootNode);
            bRaiseMouseEvents = XMLUtility.getElementBoolValue(RAISE_MOUSE_EVENTS_ELEMENT, rootNode);
        }

        /// <summary>
        /// Called when the IR state changes, i.e. when an IR source is turned on or off.
        /// We raise touch-down or touch-up events depending on what has happened.
        /// </summary>
        private void onIRStateChanged(object sender, CalibratedIRManager.IRArgs args)
        {
            // If the IR source on or off?
            if (args.State == true)
            {
                if (TouchDownEvent != null) TouchDownEvent(this, getTouchArgsFromIRArgs(args));
            }
            else
            {
                if (TouchUpEvent != null) TouchUpEvent(this, getTouchArgsFromIRArgs(args));
            }
        }

        /// <summary>
        /// Called when we get an update to the position of one of the IR sources. We
        /// raise a touch-move event.
        /// </summary>
        private void onIRPositionChanged(object sender, CalibratedIRManager.IRArgs args)
        {
            if (TouchMoveEvent != null) TouchMoveEvent(this, getTouchArgsFromIRArgs(args));
        }

        /// <summary>
        /// Converts IRArgs that we get from the CalibratedIRManager's events into
        /// TouchArgs that we need to prvide with our events.
        /// </summary>
        private TouchArgs getTouchArgsFromIRArgs(CalibratedIRManager.IRArgs args)
        {
            TouchArgs result = new TouchArgs();
            result.Button = TouchArgs.ButtonType.LEFT; // All IR events simulate the left-button
            result.PointIndex = args.Source;
            result.ScreenPosition = Utility.copy(args.ScreenPoint);
            result.Pressure = 1.0; // The IR is not pressure sensitive, so we report full pressure.
            return result;
        }

        /// <summary>
        /// Called when the config of the IRManager changes. We notify our observers with 
        /// the new XML config.
        /// </summary>
        void onIRConfigChanged(object sender, CalibratedIRManager.IRArgs e)
        {
            string strSourceName = getSourceName();
            string strXMLConfig = getXMLConfig();
            Utility.CrossThreadInvoke(ConfigChangedEvent, this, new ConfigChangedArgs { SourceName = strSourceName, XMLConfig = strXMLConfig });
        }

        #endregion 

        #region Private data

        // Object that manages the connection to the wiimotes...
        private CalibratedIRManager m_IRManager = null;

        #endregion

        #region Private constants

        private const string ROOT_ELEMENT = "WiimoteIRConfig";
        private const string CALIBRATION_DATA_ELEMENT = "CalibrationData";
        private const string CALIBRATION_IMAGE_FILENAME_ELEMENT = "CalibrationImageFilename";
        private const string SMOOTHING_LEVEL_ELEMENT = "SmoothingLevel";
        private const string RAISE_MOUSE_EVENTS_ELEMENT = "RaiseMouseEvents";

        #endregion

    }
}
